/**
 * @link 取消请求参考链接：https://blog.csdn.net/lu2925028830/article/details/126296846
 * @function 功能点：异常消息提示、统一get和post参数写法、失败后尝试重新请求（有最大次数限制）、取消请求、全局Loading兜底、默认配置（支持配置扩展）
 */

import qs from "qs";
import axios from "axios";
import type { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse, Method } from "axios";
import { typeOf, storage } from "~/utils";
import { showLoading, hideLoading, showToast, getResData, defaultResDataMap, goLogin, type ResStruct } from "./_utils";
import type { GetRequired } from "@/_types";

const enableMock = true; // 是否启用mock

// 可选的自定义请求配置
export interface CustomRequestConfig {
  isStringify?: boolean; // 是否字符串序列化
  loadEnable?: boolean; // 是否启用loading
  toastEnable?: boolean; // 当返回状态码不为成功时，是否进行弹窗提示
  reconnectMax?: number; // 请求失败后的最大尝试重新请求次数
  intercept?: boolean; // 是否拦截{code, msg, data}这层数据
  resDataMap?: {
    code: any;
    data: any;
    msg: any;
  };
}

const defaultCustomCfg: CustomRequestConfig = Object.freeze({
  isStringify: false, // 对于post请求的参数，get请求的数组或对象参数，是否字符串序列化
  loadEnable: true, // 是否启用全局loading
  toastEnable: true, // 对于code非成功值的提示信息是否要进行toast提示
  reconnectMax: 0, //失败后重新请求的最大尝试次数
  intercept: true,
  resDataMap: defaultResDataMap,
});

const statusMap: Record<number, string> = {
  400: "请求失败，请重试",
  401: "token已过期，请重新登录",
  403: "操作无权限",
  404: "资源不存在",
  405: "请求方式错误，请重试",
  408: "请求超时，请重试",
  500: "服务异常",
  502: "网关错误",
  503: "服务不可用",
  504: "网关超时",
};

let controller: any = null;

/**
 * 发送http请求
 * @param {String} method 请求类型（区分大小写），可选值：get,post,put,patch,delete
 * - get 获取数据。请求指定的页面信息，并返回实体主体。
 * - post 提交数据。如：提交表单或上传文件。数据被包含在请求体中
 * - put 更新全部数据（类似post）
 * - patch 更新局部数据（类似post，只针对更改过的）。是对put的补充，patch意为修补。
 * - delete 删除数据。
 * @param {String} url 请求地址
 * @param {Object} params 请求参数
 * @param {Object} customCfg 自定义的携带在axios config上的属性
 * @param {Object} othersCfg 除上述三个axios配置参数外，其余任意多个axios的标准参数
 * @param {number} retryCount 请求失败后，尝试重新请求次数
 * @returns
 */
export default function http<T>(
  method: Method,
  url: string,
  params: any,
  othersCfg?: AxiosRequestConfig,
  customCfg?: CustomRequestConfig,
  retryCount = 0
): Promise<T | ResStruct<T>> {
  customCfg = Object.assign({}, defaultCustomCfg, customCfg);
  const { loadEnable, toastEnable, isStringify, intercept, resDataMap, reconnectMax } = customCfg;
  const service: AxiosInstance = axios.create({
    withCredentials: true,
    // timeout: 20 * 1000,
    // baseURL: '/api',
    // baseURL: 'https://some-domain.com/api/',
    // paramsSerializer: function (params) {
    //   // query = qs.stringify(params,{arrayFormat:'repeat'})
    //   // return qs.stringify(params, { indices: false });
    //   return qs.stringify(params, { arrayFormat: "repeat" });
    // },
  });
  service.interceptors.request.use(
    (config: InternalAxiosRequestConfig) => {
      loadEnable && showLoading();
      // config.headers["Content-Type"] = "application/json;charset=utf-8";
      const token = storage.getItem("token");
      config.headers.token = token;
      config.headers["x-csrf-token"] = storage.getItem("csrfToken", "cookie");
      // config.headers.Cookie = `csrfToken=${token}`;
      return config;
    },
    (err: AxiosError) => {
      console.error("请求错误：", err);
      return err;
    }
  );

  service.interceptors.response.use(
    (res: AxiosResponse) => {
      const { data: resData } = res;
      loadEnable && hideLoading();
      if (!intercept) return resData;
      const { code, data, msg, successCode } = getResData(resData, resDataMap);
      if (code === successCode) return data;
      const newMsg = msg || "请求错误";
      toastEnable ? showToast(newMsg) : console.error(newMsg);
      return Promise.reject(msg);
    },
    (err: AxiosError) => {
      if (axios.isCancel(err)) return Promise.reject(err);
      const { status, config, code } = err;
      loadEnable && hideLoading();
      // 未授权，去登录
      if (status === 401) goLogin();
      else {
        const errMsg = code === "ECONNABORTED" ? "请求超时，请重试" : statusMap[status] ?? "请求失败";
        console.error(`${errMsg}：${(config as any).url}`, err);
        showToast(errMsg);
      }
      return Promise.reject(err);
    }
  );

  // 处理 config 配置项
  method = method.toLowerCase() as Method;
  controller = new AbortController();
  const cfg: AxiosRequestConfig = { url, method, signal: controller.signal, ...othersCfg };
  if (method === "get") {
    if (isStringify || enableMock) {
      // 将数组或对象字符串序列化
      for (const key in params) {
        const val = params[key];
        if (typeof val === "object") params[key] = JSON.stringify(val);
      }
    }
    cfg.params = params;
  } else if (["post", "put", "patch", "delete"].includes(method)) {
    const isObj = typeOf(params) === "Object";
    cfg.data = isObj && isStringify ? qs.stringify(params) : params;
  } else {
    throw new Error(`暂不支持此请求方法：${method}`);
  }
  return service(cfg).catch(err => {
    const isThrowErr = axios.isCancel(err) || retryCount >= reconnectMax!;
    if (isThrowErr) return Promise.reject(err);
    console.warn(`接口请求失败，尝试第${retryCount + 1}次重新请求`);
    return http(method, url, params, othersCfg, customCfg, retryCount + 1);
  });
}

/**
 * 取消请求
 * @returns
 */
export function cancelHttp() {
  controller.abort();
}
